# Part of Odoo. See LICENSE file for full copyright and licensing details.

from unittest.mock import patch

from odoo.fields import Command
from odoo.tests import tagged

from odoo.addons.payment.const import REPORT_REASONS_MAPPING
from odoo.addons.payment.tests.common import PaymentCommon


@tagged('-at_install', 'post_install')
class TestPaymentProvider(PaymentCommon):

    def test_changing_provider_state_archives_tokens(self):
        """ Test that all active tokens of a provider are archived when its state is changed. """
        for old_state in ('enabled', 'test'):  # No need to check when the provided was disabled.
            for new_state in ('enabled', 'test', 'disabled'):
                if old_state != new_state:  # No need to check when the state is unchanged.
                    self.provider.state = old_state
                    token = self._create_token()
                    self.provider.state = new_state
                    self.assertFalse(token.active)

    def test_enabling_provider_activates_default_payment_methods(self):
        """ Test that the default payment methods of a provider are activated when it is
        enabled. """
        self.payment_methods.active = False
        for new_state in ('enabled', 'test'):
            self.provider.state = 'disabled'
            with patch(
                'odoo.addons.payment.models.payment_provider.PaymentProvider'
                '._get_default_payment_method_codes', return_value={self.payment_method_code},
            ):
                self.provider.state = new_state
                self.assertTrue(self.payment_methods.active)

    def test_enabling_manual_capture_provider_activates_compatible_default_pms(self):
        """Test that only payment methods supporting manual capture are activated when a provider
        requiring manual capture is enabled."""
        payment_method_with_manual_capture = self.env['payment.method'].create({
            'name': 'Payment Method With Manual Capture',
            'code': 'pm_with_manual_capture',
            'support_manual_capture': 'full_only',
        })
        self.provider.state = 'disabled'
        self.provider.capture_manually = True
        self.provider.payment_method_ids = [Command.set([
            self.payment_method.id, payment_method_with_manual_capture.id
        ])]
        self.payment_method.support_manual_capture = 'none'
        default_codes = {self.payment_method_code, payment_method_with_manual_capture.code}
        with patch(
            'odoo.addons.payment.models.payment_provider.PaymentProvider'
            '._get_default_payment_method_codes', return_value=default_codes,
        ):
            self.provider.state = 'test'
            self.assertFalse(self.payment_methods.active)
            self.assertTrue(payment_method_with_manual_capture.active)

    def test_disabling_provider_deactivates_default_payment_methods(self):
        """ Test that the default payment methods of a provider are deactivated when it is
        disabled. """
        self.payment_methods.active = True
        for old_state in ('enabled', 'test'):
            self.provider.state = old_state
            with patch(
                'odoo.addons.payment.models.payment_provider.PaymentProvider'
                '._get_default_payment_method_codes', return_value=self.payment_method_code,
            ):
                self.provider.state = 'disabled'
                self.assertFalse(self.payment_methods.active)

    def test_enabling_provider_activates_processing_cron(self):
        """ Test that the post-processing cron is activated when a provider is enabled. """
        self.env['payment.provider'].search([]).state = 'disabled'  # Reset providers' state.
        post_processing_cron = self.env.ref('payment.cron_post_process_payment_tx')
        for enabled_state in ('enabled', 'test'):
            post_processing_cron.active = False  # Reset the cron's active field.
            self.provider.state = 'disabled'  # Prepare the dummy provider for enabling.
            self.provider.state = enabled_state
            self.assertTrue(post_processing_cron.active)

    def test_disabling_provider_deactivates_processing_cron(self):
        """ Test that the post-processing cron is deactivated when a provider is disabled. """
        self.env['payment.provider'].search([]).state = 'disabled'  # Reset providers' state.
        post_processing_cron = self.env.ref('payment.cron_post_process_payment_tx')
        for enabled_state in ('enabled', 'test'):
            post_processing_cron.active = True  # Reset the cron's active field.
            self.provider.state = enabled_state  # Prepare the dummy provider for disabling.
            self.provider.state = 'disabled'
            self.assertFalse(post_processing_cron.active)

    def test_published_provider_compatible_with_all_users(self):
        """ Test that a published provider is always available to all users. """
        for user in (self.public_user, self.portal_user):
            self.env = self.env(user=user)

            compatible_providers = self.env['payment.provider'].sudo()._get_compatible_providers(
                self.company.id, self.partner.id, self.amount
            )
            self.assertIn(self.provider, compatible_providers)

    def test_unpublished_provider_compatible_with_internal_user(self):
        """ Test that an unpublished provider is still available to internal users. """
        self.provider.is_published = False

        compatible_providers = self.env['payment.provider']._get_compatible_providers(
            self.company.id, self.partner.id, self.amount
        )
        self.assertIn(self.provider, compatible_providers)

    def test_unpublished_provider_not_compatible_with_non_internal_user(self):
        """ Test that an unpublished provider is not available to non-internal users. """
        self.provider.is_published = False
        for user in (self.public_user, self.portal_user):
            self.env = self.env(user=user)

            compatible_providers = self.env['payment.provider'].sudo()._get_compatible_providers(
                self.company.id, self.partner.id, self.amount
            )
            self.assertNotIn(self.provider, compatible_providers)

    def test_provider_compatible_with_branch_companies(self):
        """ Test that the provider is available to branch companies. """
        branch_company = self.env['res.company'].create({
            'name': "Provider Branch Company",
            'parent_id': self.provider.company_id.id,
        })
        compatible_providers = self.provider._get_compatible_providers(
            branch_company.id, self.partner.id, self.amount,
        )
        self.assertIn(self.provider, compatible_providers)

    def test_provider_compatible_with_available_countries(self):
        """ Test that the provider is compatible with its available countries. """
        belgium = self.env.ref('base.be')
        self.provider.available_country_ids = [Command.set([belgium.id])]
        self.partner.country_id = belgium
        compatible_providers = self.provider._get_compatible_providers(
            self.company.id, self.partner.id, self.amount
        )
        self.assertIn(self.provider, compatible_providers)

    def test_provider_not_compatible_with_unavailable_countries(self):
        """ Test that the provider is not compatible with a country that is not available. """
        belgium = self.env.ref('base.be')
        self.provider.available_country_ids = [Command.set([belgium.id])]
        france = self.env.ref('base.fr')
        self.partner.country_id = france
        compatible_providers = self.provider._get_compatible_providers(
            self.company.id, self.partner.id, self.amount
        )
        self.assertNotIn(self.provider, compatible_providers)

    def test_provider_compatible_when_no_available_countries_set(self):
        """ Test that the provider is always compatible when no available countries are set. """
        self.provider.available_country_ids = [Command.clear()]
        belgium = self.env.ref('base.be')
        self.partner.country_id = belgium
        compatible_providers = self.provider._get_compatible_providers(
            self.company.id, self.partner.id, self.amount
        )
        self.assertIn(self.provider, compatible_providers)

    def test_provider_compatible_when_maximum_amount_is_zero(self):
        """ Test that the maximum amount has no effect on the provider's compatibility when it is
        set to 0. """
        self.provider.maximum_amount = 0.
        currency = self.provider.main_currency_id.id

        compatible_providers = self.env['payment.provider']._get_compatible_providers(
            self.company.id, self.partner.id, self.amount, currency_id=currency
        )
        self.assertIn(self.provider, compatible_providers)

    def test_provider_compatible_when_payment_below_maximum_amount(self):
        """ Test that a provider is compatible when the payment amount is less than the maximum
        amount. """
        self.provider.maximum_amount = self.amount + 10.0
        currency = self.provider.main_currency_id.id

        compatible_providers = self.env['payment.provider']._get_compatible_providers(
            self.company.id, self.partner.id, self.amount, currency_id=currency
        )
        self.assertIn(self.provider, compatible_providers)

    def test_provider_not_compatible_when_payment_above_maximum_amount(self):
        """ Test that a provider is not compatible when the payment amount is more than the maximum
        amount. """
        self.provider.maximum_amount = self.amount - 10.0
        currency = self.provider.main_currency_id.id

        compatible_providers = self.env['payment.provider']._get_compatible_providers(
            self.company.id, self.partner.id, self.amount, currency_id=currency
        )
        self.assertNotIn(self.provider, compatible_providers)

    def test_provider_compatible_with_available_currencies(self):
        """ Test that the provider is compatible with its available currencies. """
        compatible_providers = self.provider._get_compatible_providers(
            self.company.id, self.partner.id, self.amount, currency_id=self.currency_euro.id
        )
        self.assertIn(self.provider, compatible_providers)

    def test_provider_not_compatible_with_unavailable_currencies(self):
        """ Test that the provider is not compatible with a currency that is not available. """
        # Make sure the list of available currencies is not empty.
        self.provider.available_currency_ids = [Command.unlink(self.currency_usd.id)]
        compatible_providers = self.provider._get_compatible_providers(
            self.company.id, self.partner.id, self.amount, currency_id=self.currency_usd.id
        )
        self.assertNotIn(self.provider, compatible_providers)

    def test_provider_compatible_when_no_available_currencies_set(self):
        """ Test that the provider is always compatible when no available currency is set. """
        self.provider.available_currency_ids = [Command.clear()]
        compatible_providers = self.provider._get_compatible_providers(
            self.company.id, self.partner.id, self.amount, currency_id=self.currency_euro.id
        )
        self.assertIn(self.provider, compatible_providers)

    def test_provider_compatible_when_tokenization_forced(self):
        """ Test that the provider is compatible when it allows tokenization while it is forced by
        the calling module. """
        self.provider.allow_tokenization = True
        compatible_providers = self.provider._get_compatible_providers(
            self.company.id, self.partner.id, self.amount, force_tokenization=True
        )
        self.assertIn(self.provider, compatible_providers)

    def test_provider_not_compatible_when_tokenization_forced(self):
        """ Test that the provider is not compatible when it does not allow tokenization while it
        is forced by the calling module. """
        self.provider.allow_tokenization = False
        compatible_providers = self.provider._get_compatible_providers(
            self.company.id, self.partner.id, self.amount, force_tokenization=True
        )
        self.assertNotIn(self.provider, compatible_providers)

    def test_provider_compatible_when_tokenization_required(self):
        """ Test that the provider is compatible when it allows tokenization while it is required by
        the payment context (e.g., when paying for a subscription). """
        self.provider.allow_tokenization = True
        with patch(
            'odoo.addons.payment.models.payment_provider.PaymentProvider._is_tokenization_required',
            return_value=True,
        ):
            compatible_providers = self.provider._get_compatible_providers(
                self.company.id, self.partner.id, self.amount
            )
        self.assertIn(self.provider, compatible_providers)

    def test_provider_not_compatible_when_tokenization_required(self):
        """ Test that the provider is not compatible when it does not allow tokenization while it
        is required by the payment context (e.g., when paying for a subscription). """
        self.provider.allow_tokenization = False
        with patch(
            'odoo.addons.payment.models.payment_provider.PaymentProvider._is_tokenization_required',
            return_value=True,
        ):
            compatible_providers = self.provider._get_compatible_providers(
                self.company.id, self.partner.id, self.amount
            )
        self.assertNotIn(self.provider, compatible_providers)

    def test_provider_compatible_with_express_checkout(self):
        """ Test that the provider is compatible when it allows express checkout while it is an
        express checkout flow. """
        self.provider.allow_express_checkout = True
        compatible_providers = self.provider._get_compatible_providers(
            self.company.id, self.partner.id, self.amount, is_express_checkout=True
        )
        self.assertIn(self.provider, compatible_providers)

    def test_provider_not_compatible_with_express_checkout(self):
        """ Test that the provider is not compatible when it does not allow express checkout while
        it is an express checkout flow. """
        self.provider.allow_express_checkout = False
        compatible_providers = self.provider._get_compatible_providers(
            self.company.id, self.partner.id, self.amount, is_express_checkout=True
        )
        self.assertNotIn(self.provider, compatible_providers)

    def test_availability_report_covers_all_reasons(self):
        """ Test that every possible unavailability reason is correctly reported. """
        # Disable all providers.
        providers = self.env['payment.provider'].search([])
        providers.state = 'disabled'

        # Prepare a base provider.
        self.provider.write({
            'state': 'test',
            'allow_express_checkout': True,
            'allow_tokenization': True,
        })

        # Prepare a provider with an incompatible country.
        invalid_country_provider = self.provider.copy()
        belgium = self.env.ref('base.be')
        invalid_country_provider.write({
            'state': 'test',
            'available_country_ids': [Command.set([belgium.id])],
        })
        france = self.env.ref('base.fr')
        self.partner.country_id = france

        # Prepare a provider with a maximum amount lower than the payment amount.
        exceeding_max_provider = self.provider.copy()
        exceeding_max_provider.write({
            'state': 'test',
            'maximum_amount': self.amount - 10.0,
        })

        # Prepare a provider with an incompatible currency.
        invalid_currency_provider = self.provider.copy()
        invalid_currency_provider.write({
            'state': 'test',
            'available_currency_ids': [Command.unlink(self.currency_usd.id)],
        })

        # Prepare a provider without tokenization support.
        no_tokenization_provider = self.provider.copy()
        no_tokenization_provider.write({
            'state': 'test',
            'allow_tokenization': False,
        })

        # Prepare a provider without express checkout support.
        no_express_checkout_provider = self.provider.copy()
        no_express_checkout_provider.write({
            'state': 'test',
            'allow_express_checkout': False,
        })

        # Get compatible providers to generate their availability report.
        report = {}
        self.env['payment.provider']._get_compatible_providers(
            self.company_id,
            self.partner.id,
            self.amount,
            currency_id=self.currency_usd.id,
            force_tokenization=True,
            is_express_checkout=True,
            report=report,
        )

        # Compare the generated providers report with the expected one.
        expected_providers_report = {
            self.provider: {
                'available': True,
                'reason': '',
            },
            invalid_country_provider: {
                'available': False,
                'reason': REPORT_REASONS_MAPPING['incompatible_country'],
             },
            exceeding_max_provider: {
                'available': False,
                'reason': REPORT_REASONS_MAPPING['exceed_max_amount'],
            },
            invalid_currency_provider: {
                'available': False,
                'reason': REPORT_REASONS_MAPPING['incompatible_currency'],
             },
            no_tokenization_provider: {
                'available': False,
                'reason': REPORT_REASONS_MAPPING['tokenization_not_supported'],
             },
            no_express_checkout_provider: {
                'available': False,
                'reason': REPORT_REASONS_MAPPING['express_checkout_not_supported'],
            },
        }
        self.maxDiff = None
        self.assertDictEqual(report['providers'], expected_providers_report)

    def test_validation_currency_is_supported(self):
        """ Test that only currencies supported by both the provider and the payment method can be
        used in validation operations. """
        self.provider.available_currency_ids = [Command.clear()]  # Supports all currencies.
        self.payment_method.supported_currency_ids = [Command.clear()]  # Supports all currencies.
        validation_currency = self.provider.with_context(
            validation_pm=self.payment_method
        )._get_validation_currency()
        self.assertEqual(validation_currency, self.provider.company_id.currency_id)

        self.provider.available_currency_ids = [Command.set(self.currency_usd.ids)]
        self.payment_method.supported_currency_ids = [Command.clear()]  # Supports all currencies.
        validation_currency = self.provider.with_context(
            validation_pm=self.payment_method
        )._get_validation_currency()
        self.assertIn(validation_currency, self.provider.available_currency_ids)

        self.provider.available_currency_ids = [Command.clear()]  # Supports all currencies.
        self.payment_method.supported_currency_ids = [Command.set(self.currency_usd.ids)]
        validation_currency = self.provider.with_context(
            validation_pm=self.payment_method
        )._get_validation_currency()
        self.assertIn(validation_currency, self.payment_method.supported_currency_ids)

        self.provider.available_currency_ids = [Command.set(self.currency_usd.ids)]
        self.payment_method.supported_currency_ids = [Command.set(self.currency_usd.ids)]
        validation_currency = self.provider.with_context(
            validation_pm=self.payment_method
        )._get_validation_currency()
        self.assertIn(validation_currency, self.provider.available_currency_ids)
        self.assertIn(validation_currency, self.payment_method.supported_currency_ids)
